import type { CustomRequestOptions } from '@/interceptors/request'
// 刷新 token 使用 isRefreshRequest 就是一个判断是不是 refresh token 的request
import { isRefreshRequest, refreshTokenApi } from '@/api/loginApi'
import { useUserStore } from '@/store'
import { merge } from 'lodash-es'

type CustomRequestOptionsOmit = Omit<CustomRequestOptions, 'url' | 'method'>
type ThenType<T> = <TResult1 = T, TResult2 = never>(
  onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
  onrejected?: ((reason: T) => TResult2 | PromiseLike<TResult2> | T) | undefined | null,
) => CancellablePromise<TResult1 | TResult2>

type ThenTypeOnlyData<T> = <TResult1 = T, TResult2 = never>(
  onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
  onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2> | T) | undefined | null,
) => CancellablePromise<TResult1 | TResult2>

export interface CancellablePromise<T> extends Promise<T> {
  abort: (error?: any) => void
  then: ThenType<T>
  catch: <TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null) => Promise<T | TResult>

  [K: string | number | symbol]: any
}

export interface CancellablePromiseOnlyData<T> extends Promise<T> {
  abort: (error?: any) => void
  then: ThenTypeOnlyData<T>
  catch: <TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null) => Promise<T | TResult>

  [K: string | number | symbol]: any
}

export type HttpOptionsType = Omit<CustomRequestOptions, 'isBaseUrl'>

// HTTP 一些默认配置
const _config: HttpConfig = {
  loginUrl: '/pages/login/login',
  redirectToLogin: true,
  hideErrorToast: false,
  request: {
    accessTokenKey: 'accessToken',
    refreshTokenKey: 'refreshToken',
  },
  status: {
    key: 'code',
    success: 200,
    logout: 401,
  },
  response: {
    messageKey: 'message',
    dataKey: 'data',
  },
}

let refreshing = false // 防止重复刷新 token 标示
let taskQueue = [] // 刷新 token 请求队列
let _redirectTimer = null

export class http {
  private static httpConfig: HttpConfig = _config

  public static setup(configOptions?: HttpConfig) {
    merge(this.httpConfig, configOptions)
  };

  public static config(): HttpConfig {
    return this.httpConfig
  }

  private static getRefreshTokenValue(refreshTokenKey: string) {
    const store = useUserStore()
    let refreshToken = (store.userInfo || {})[refreshTokenKey] || ''
    if (!refreshToken) {
      refreshToken = uni.getStorageSync(refreshTokenKey) || ''
    }
    return refreshToken
  }

  private static http<T>(options: HttpOptionsType = { url: '' }) {
    const statusKey = options.httpConfig?.status?.key ?? this.config().status?.key ?? 'code'
    const successValue = options.httpConfig?.status?.success ?? this.config().status?.success ?? 200
    const logoutCode = options.httpConfig?.status?.logout ?? this.config().status?.logout ?? 401

    const dataKey = options.httpConfig?.response?.dataKey ?? this.config().response?.dataKey ?? 'data'
    const messageKey = options.httpConfig?.response?.messageKey ?? this.config().response?.messageKey ?? 'message'
    const onlyReturnData = options.onlyData ?? false

    const refreshTokenKey = options.httpConfig?.request?.refreshTokenKey ?? this.config().request?.refreshTokenKey
    const redirectToLogin = options.httpConfig?.redirectToLogin ?? this.config().redirectToLogin ?? false
    const loginUrl = this.config().loginUrl ?? ''

    const hideErrorToast = options.hideErrorToast ?? this.config().hideErrorToast ?? false
    // 调试开关
    const debug = options.debug ?? false
    // 调试输出的格式
    const debugFormat = options.debugFormat ?? 'obj'
    // 是否是json格式
    const debugFormatJson = debugFormat === 'json'
    // 调试传入的 options 参数
    const debugOptions = options.debugOptions ?? false
    // 调试请求结果
    const debugResponse = options.debugResponse ?? false

    let requestTask: any
    const promise = new Promise<T>((resolve, reject) => {
      requestTask = uni.request({
        ...options,
        success: async (res: any) => {
          // 这里是调试相关的
          if (debug || debugOptions) {
            debugFormatJson ? console.info(JSON.stringify(options, null, '\t')) : console.info(options)
          }
          if (debug || debugResponse) {
            debugFormatJson ? console.info(JSON.stringify(res, null, '\t')) : console.info(res)
          }
          /* -------- 请求成功 ----------- */
          if ((res.statusCode >= 200 && res.statusCode < 300) || res.data[statusKey] === successValue) {
            // 如果是约定的成功值，比如 code === 200 就返回
            if (res.data[statusKey] === successValue) {
              return onlyReturnData ? resolve(res.data[dataKey] as unknown as T) : resolve(res.data as unknown as T)
            }
            // 如果不是正常状态就 reject
            else {
              !hideErrorToast && uni.showToast({
                title: res.data[messageKey] || '请求异常',
                icon: 'none',
              })
              return onlyReturnData ? reject(res.data[dataKey] as unknown as T) : reject(res.data as unknown as T)
            }
          }
          /* -------- 无感刷新 token 配置了才执行 ----------- */
          if (typeof refreshTokenKey === 'string' && refreshTokenKey.length > 0 && refreshTokenKey !== 'false') {
            const refreshToken = http.getRefreshTokenValue(refreshTokenKey)
            // token 失效的，且有刷新 token 的，才放到请求队列里
            if ((res.data[statusKey] === logoutCode || res.statusCode === 401) && refreshToken !== '') {
              taskQueue.push(() => {
                resolve(this.http<T>(options))
              })
            }
            // 只有不是refresh token 的请求才能进这里
            if (!isRefreshRequest(options) && (res.data[statusKey] === logoutCode || res.statusCode === 401) && refreshToken !== '' && !refreshing) {
              // 这个变量做工是保证一个时候只有一个刷新请求在做，因为刷新token也是要时间的
              refreshing = true
              // 发起刷新 token 请求
              const [refreshTokenRes, refreshTokenErr] = await refreshTokenApi()
              refreshing = false
              // 刷新 token 成功，将任务队列的所有任务重新请求，判断 刷新Token 到底刷新到没有
              if (refreshTokenRes && refreshTokenRes[statusKey] === successValue) {
                if (!hideErrorToast) {
                  nextTick(() => {
                    // 关闭其他弹窗
                    uni.hideToast()
                    // 刷新 token 失败，跳转到登录页
                    uni.showToast({
                      title: 'token 刷新成功，重载中',
                      icon: 'none',
                    })
                  })
                }
                // 恢复之前的请求
                taskQueue.forEach((event) => {
                  event()
                })
              }
              // 这里如果刷新token 也失败了。那就真的就是无权限了，走登录页面，什么的
              if (refreshTokenErr) {
                if (!hideErrorToast) {
                  // 刷新 token 失败，跳转到登录页
                  nextTick(() => {
                    // 关闭其他弹窗
                    uni.hideToast()
                    // 刷新 token 失败，跳转到登录页
                    uni.showToast({
                      title: '登录已过期，请重新登录',
                      icon: 'none',
                    })
                  })
                }
                _redirectTimer && clearTimeout(_redirectTimer)
                _redirectTimer = setTimeout(() => {
                  // 清除 用户信息（包括 token）
                  const store = useUserStore()
                  store.clearUserInfo()
                  // 跳转到登录页
                  if (redirectToLogin) {
                    const loginParamUrl = loginUrl.includes('?') ? `${loginUrl}&from=refresh` : `${loginUrl}?from=refresh`
                    uni.redirectTo({ url: loginParamUrl })
                  }
                }, !hideErrorToast ? 2000 : 50)
              }
              // 不管刷新 token 成功与否，都清空任务队列
              taskQueue = []
              // 刷新成功否，都不往下走了。不然会多一个提示
              return
            }
          }

          // 默认没有登录的处理
          if ((res.data[statusKey] === logoutCode || res.statusCode === 401) && (typeof refreshTokenKey === 'string' && refreshTokenKey.length > 0 && refreshTokenKey !== 'false')) {
            if (!hideErrorToast) {
              // 刷新 token 失败，跳转到登录页
              nextTick(() => {
                // 关闭其他弹窗
                uni.hideToast()
                // 刷新 token 失败，跳转到登录页
                uni.showToast({
                  title: '登录已过期，请重新登录',
                  icon: 'none',
                })
              })
            }
            const store = useUserStore()
            const refreshToken = http.getRefreshTokenValue(refreshTokenKey)
            if (refreshToken === '') {
              // 清除 用户信息（包括 token）
              store.clearUserInfo()
              // 跳转到登录页
              if (redirectToLogin) {
                const loginParamUrl = loginUrl.includes('?') ? `${loginUrl}&from=refresh` : `${loginUrl}?from=refresh`
                uni.redirectTo({ url: loginParamUrl })
              }
              return
            }
          }
          /* -------- 剩余情况都默认请求异常 ----------- */
          !hideErrorToast && uni.showToast({
            title: res.data[messageKey] || '请求异常',
            icon: 'none',
          })
          if (onlyReturnData) {
            reject(res.data[dataKey] as unknown as T)
          }
          else {
            reject({
              [statusKey]: res.statusCode ?? res.data[statusKey] ?? 500,
              [messageKey]: '系统错误',
              [dataKey]: res.data && res.data[dataKey] || null,
            } as unknown as T)
          }
        },
        fail: (err) => {
          if (err.errMsg === 'request:fail abort') {
            console.log(`请求 ${options.url} 被取消`)
          }
          else {
            if (onlyReturnData) {
              reject(err.errMsg as unknown as T)
            }
            else {
              reject({
                [statusKey]: 500,
                [messageKey]: '系统错误',
                [dataKey]: err,
              } as unknown as T)
            }
          }
        },
      })
    })
    return {
      ...promise, // 补全 Promise 的类型
      then: promise.then.bind(promise) as ThenType<T>,
      catch: promise.catch.bind(promise) as CancellablePromise<T>['catch'],
      finally: promise.finally.bind(promise) as CancellablePromise<T>['finally'],
      [Symbol.iterator]: promise[Symbol.iterator],
      abort: () => {
        // 取消请求
        requestTask.abort()
      },
    }
  }

  // 这里包装一下，直接返回包装类型
  public static fetch<T>(url: string, options?: CustomRequestOptionsOmit): CancellablePromise<T> {
    return this.http<T>({
      url,
      ...options,
    })
  }

  // GET
  public static get<T>(url: string, options?: CustomRequestOptionsOmit) {
    return this.http<ResData<T>>({
      url,
      method: 'GET',
      ...options,
      onlyData: false,
    })
  }

  // GET
  public static getData<T>(url: string, options?: CustomRequestOptionsOmit): CancellablePromiseOnlyData<T> {
    return this.http<T>({
      url,
      method: 'GET',
      ...options,
      onlyData: true,
    })
  }

  // POST
  public static post<T>(url: string, options?: CustomRequestOptionsOmit) {
    return this.http<ResData<T>>({
      url,
      method: 'POST',
      ...options,
      onlyData: false,
    })
  }

  public static postData<T>(url: string, options?: CustomRequestOptionsOmit): CancellablePromiseOnlyData<T> {
    return this.http<T>({
      url,
      method: 'POST',
      ...options,
      onlyData: true,
    })
  }

  // PUT
  public static put<T>(url: string, options?: CustomRequestOptionsOmit) {
    return this.http<ResData<T>>({
      url,
      method: 'PUT',
      ...options,
      onlyData: false,
    })
  }

  public static putData<T>(url: string, options?: CustomRequestOptionsOmit): CancellablePromiseOnlyData<T> {
    return this.http<T>({
      url,
      method: 'PUT',
      ...options,
      onlyData: true,
    })
  }

  // DELETE
  public static delete<T>(url: string, options?: CustomRequestOptionsOmit) {
    return this.http<ResData<T>>({
      url,
      method: 'DELETE',
      ...options,
      onlyData: false,
    })
  }

  public static deleteData<T>(url: string, options?: CustomRequestOptionsOmit): CancellablePromiseOnlyData<T> {
    return this.http<T>({
      url,
      method: 'DELETE',
      ...options,
      onlyData: true,
    })
  }
}
